<?php

namespace PHPSpring\Core;

use Error;
use PHPSpring\Utils\Utils;
use ReflectionClass;
use ReflectionMethod;

class Proxy {

  private static array $proxyMap = [];

  private static string $proxyTpl = '
namespace ${namespace};

class ${className} extends ${targetClass} {
  private $target;

  public function __construct($target) {
    if (is_string($target)) {
      $target = new $target();
    }
    $this->target = $target;
    \PHPSpring\Core\AOP::postInit($target);
  }

${methods}
}';

  public static function getProxy ($target) {
    $targetClassName = self::getClassName($target);

    if (isset(self::$proxyMap[$targetClassName])) {
      return self::$proxyMap[$targetClassName];
    }

    $proxy = self::genProxy($targetClassName);
    // var_dump($proxy);
    eval($proxy['class']);
    self::$proxyMap[$targetClassName] = $proxy['name'];

    return self::$proxyMap[$targetClassName];
  }

  private static function genProxy ($className) {
    $re = new ReflectionClass($className);
    if ($re->isFinal()) {
      throw new Error('Can not proxy a final class'.$className);
    }
    $methods = [];
    foreach ($re->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
      if ($re->name != $method->class) {
        // TODO
        // continue;
      }
      if ($method->isFinal()) {
        throw new Error('Can not proxy a final method'.$method->name);
      }
      $retType = Utils::normalizeReturnType($method->getReturnType());
      $params = [];
      $invokeArgs = [];
      foreach ($method->getParameters() as $param) {
        $type = Utils::normalizeParameterType($param->getType());
        $defaultVal = null;
        if ($param->isOptional()) {
          $defaultVal = $param->getDefaultValueConstantName();
          if ($defaultVal === null) {
            $defaultVal = $param->getDefaultValue();
            if (is_string($defaultVal)) {
              $defaultVal = '"'.$defaultVal.'"';
            } elseif (is_null($defaultVal)) {
              $defaultVal = 'null';
            }
          }
          $defaultVal = "=$defaultVal";
        }
        $params[] = $type.'$'.$param->name.($defaultVal !== null ? $defaultVal : '');
        $invokeArgs[] = '$'.$param->name;

      }
      $paramStr = implode(', ', $params);
      $invokeArgsStr = implode(', ', $invokeArgs);
      $methodName = $method->name;
      $ns = __NAMESPACE__;
      $methods[] = "  public function $methodName($paramStr)$retType {".PHP_EOL
        ."    return \\$ns\\AOP::invoke(\$this->target, '$methodName',  array($invokeArgsStr));".PHP_EOL
        ."  }";
    }
    $replace = [
      '${namespace}'   => "$ns\\Proxys",
      '${className}'   => $re->getShortName().'Proxy',
      '${targetClass}' => $className,
      '${methods}'     => join(PHP_EOL.PHP_EOL, $methods),
    ];
    $proxyClass = str_replace(array_keys($replace), array_values($replace), self::$proxyTpl);

    return [
      'name'  => '\\'.$replace['${namespace}'].'\\'.$replace['${className}'],
      'class' => $proxyClass];
  }

  public static function getClassName ($target) {
    if (is_object($target)) {
      $targetClassName = get_class($target);
    } else {
      $targetClassName = $target;
    }
    if (substr($targetClassName, 0, 1) != '\\') {
      $targetClassName = '\\'.$targetClassName;
    }

    return $targetClassName;
  }

}
